Technology Client Server 2008'2 |
|||||||
|
“I usually have plot songs,
but this song has no plot.
It would seem a set of words.
No - this is a disturbing song. ” V. Vysotsky about the song "Sail"
Realizing myself a full account of the fact that the word “cryptanalysis”, so unceremoniously used in the title of the article, can entail a real storm of righteous criticism, I still do not hurry to part with it and do not even intend to deprive the prefix “crypto”. Of course, in itself, the WinAPI function UuidCreate, which has become the subject of this study, has nothing to do with cryptography. Her modest part is to create unique identifiers (Universally Unique Identifiers) that comply with RFC4122 requirements. However, for some reason, those who want to prick nuts with a microscope do not stop. On the contrary, I increasingly come across attempts to employ UuidCreate as a pseudo-random number generator (PRNG).
The reasons for this phenomenon, in my opinion, several. First, the values of identifiers seem random and independent (we are baptized three times). Secondly, the algorithm of the UUID-generator has not yet been published and, as far as I know, there is not a single trustworthy work where its properties would be described. Thirdly, the official and plenipotentiary Windows PRNG - the CryptGenRandom function - loses UuidCreate in simplicity and accessibility (it’s enough to remember that using CryptGenRandom implies a date with CryptoAPI, and not every programmer will end this date with a tumultuous novel).
So, for example, having met once with a colleague from a telecommunications company over a glass of good Czech beer, I learned a rather curious fact. It turns out that PIN codes for express payment cards of this “far-sighted” commercial organization are generated on MS SQL Server using the newid function (whose mask hides all the same UuidCreate), and no additional transformations are performed on the values obtained. Having decided at first that the "author" burned, I expressed emotions in the best traditions of the Przhevalsky horse, but a second later I regretted it. The bewilderment on the face of the interlocutor definitely indicated the sincerity of his words and, if I had a pocket Bible at hand, I am sure that he would swear on it. There was no Bible, so I had to swear in the latest issue of the magazine "MAXIM".
Then, unfortunately, I did not manage to find weighty arguments in favor of the inadmissibility of using UuidCreate for such purposes, and until the last drop of beer, each of us remained in our opinion. However, the revelations colleagues for a long time did not give me rest. To finally dot the i, I decided to check how feasible the following two scenarios.
The international operator of cellular and cellular communications, Give Kilo Sala, known after the re-branding under the brand name “Hello, Laundry?”, Is preparing to introduce to its subscribers a new tariff plan “Gluhonema”. In addition to the unprecedentedly low cost of one minute of conversation with the indigenous population of Franz Josef Land, the tariff gives you the opportunity to download the ringtone “No sound in the garden”.
The company is optimistic about the future and expects to increase its presence in the market of long-distance services. Anticipating a large increase in the number of subscribers, the leadership instructs the secret department "White Collars", which includes employees only with an impeccable reputation, to release a new series of express payment cards.
A special physical server is assigned to this good deed, which is prepared by system administrator Vasily Razdolbaev (Nordic character, not married).
Next, we describe the events in chronological order.
Morning, April 14, Monday. Vasily installs on the machine OS Windows and MS SQL Server 2008.
Evening is April 14, Monday. A commission from among the employees of the secret department checks the server for spyware and makes a conclusion: no malicious code was detected on the machine. The server, without shutting down, is moved to an underground bunker completely cut off from the outside world (lead walls 2 meters thick, interception of information is completely excluded, only “White collars” have access to the room).
Evening, April 15, Tuesday. Vasily goes to the most ruthless corporate event in his life, set up on the occasion of the release of the new tariff. In karaoke performs bawdy songs, dances naked on the table and clings to Masha Bucket from the accounting department. In other words, behaves like a real gentleman.
And at this time ... While the system administrator is relaxing, White Collars generate PIN codes for a new series of express payment cards. The T-SQL function NEWID is used as the source of random numbers (which, as will be shown below, is equivalent to calling UuidCreate). PIN codes received in this way are immediately applied to payment cards. When the procedure is fully completed, the server under the vigilant control of the guards armed to the teeth is taken to the nearest metallurgical plant, where it is burned in a blast furnace.
Morning, April 16, Wednesday. New express payment cards go on sale. Vasily Razdolbaev finds himself in a garbage can outside the Moscow Ring Road.
Attention, the question is: can Vasily, without resorting to rectal cryptanalysis of secret service employees, recover the PIN codes of the entire series of cards, and what does he need to do to do this?
Suppose the server is not destroyed. Moreover, the system administrator was able to access it only on Wednesday, when the cards were already on sale. Does Vasily still have a chance to get PINs, given that they were deleted from the database by “White Collars”?
To more or less clearly answer these intriguing questions, we have to look under the hood of UuidCreate.
Unfortunately, the official documentation we have at our disposal (an article in MSDN [4] and the text RFC4122 [1]) can only shed light on the structure of the return value, so there is no more benefit from it than from the previously mentioned journal MAXIM.
Function Definition (rpcdce.h):
RPCRTAPI RPC_STATUS RPC_ENTRY UuidCreate (Out UUID RPC_FAR * Uuid); |
The structure of the return parameter Uuid (guiddef.h):
typedef struct _GUID { unsigned long Data1; unsigned short Data2; unsigned short Data3; unsigned char Data4 [8]; } GUID; typedef GUID UUID; |
UUID identifiers are often written as a text string:
{G 1 G 2 G 3 G 4 -G 5 G 6 -G 7 G 8 -G 9 G 10 -G 11 G 12 G 13 G 14 G 15 G 16 }
where G x is the value of the corresponding byte of the structure in hexadecimal representation:
Data1 = G 4 G 3 G 2 G 1
Data2 = G 6 G 5
Data3 = G 8 G 7
Data4 = G 9 G 10 G 11 G 12 G 13 G 14 G 15 G 16
According to [1], the first two bits of the G 9 define the format in which it is necessary to interpret the value of the identifier. In the case of UuidCreate, they are always equal to 10, therefore, the format must fully meet the requirements of RFC4122, which gives us the right to treat the four high bits of G 7 as the version number of the algorithm. Our experimental set sequence 0100 in them (version 4, “The randomly or pseudo-randomly generated version”), so the remaining 122 bits of the structure must be filled in randomly. Is it really - now find out.
We will prepare rpcrt4.dll (see Appendix B for a list of the versions examined) using the inhuman capabilities of the IDA Pro disassembler and the Visual Studio 2005 debugger, since it is in this library that the UUID generator is hidden from prying eyes.
We begin, of course, with the UuidCreate function.
RPCRTAPI RPC_STATUS RPC_ENTRY UuidCreate ( OUT UUID __RPC_FAR * Uuid ) { RPC_STATUS ret; ret = GenerateRandomNumber; ((BYTE *) Uuid, sizeof (UUID)); if (ret == RPC_S_OK) { // Install the version Uuid-> Data3 = (Uuid-> Data3 & 0x0fff) | 0x4000; // swear allegiance to RFC4122 format Uuid-> Data4 [0] = (Uuid-> Data4 [0] & 0x3F) | 0x80; }; return ret; }; |
In addition to calling GenerateRandomNumber, the function body contains nothing new for us. We cut further.
// RC4 instance state typedef struct _RC4_INSTANCE_STATE { BYTE SBOX [256]; // S-block BYTE i; // Register RC4 BYTE j; // Register RC4 } RC4_INSTANCE_STATE, * PRC4_INSTANCE_STATE; // RC4 instance typedef struct _RC4_INSTANCE { // Number of bytes of the instance DWORD Accumulator; // Critical section for thread synchronization CRITICAL_SECTION CS; // RC4 instance status RC4_INSTANCE_STATE State; } RC4_INSTANCE, * pRC4_INSTANCE; // Generator state typedef struct _RC4_CONTEXT { DWORD InstanceCount; // Total number of RC4 instances DWORD AlwaysOne; // Reserved. Always equal to 1 pRC4_INSTANCE Rc4Ins [8]; // RC4 instances } RC4_CONTEXT, * pRC4_CONTEXT; // Global variable that determines the state of the UUID generator RC4_CONTEXT g_rc4SafeCtx; // Global variable defining the selection of the RC4 instance DWORD g_rc4TotalRequests = 0; RPC_STATUS GenerateRandomNumber (BYTE * pbData, DWORD cbData) { // Total number of bytes of output of the current RC4 instance // since its initialization DWORD Accumulator = 0; // Number of the current RC4 instance DWORD InstNum; // Buffer for RC4 key, if initialization is required PVOID RandomKey; rc4_safe_select (& g_rc4SafeCtx, & InstNum, & Accumulator); if (Accumulator> = 500000) { //Initialization // Generate a pseudo-random key for RC4 if (! SystemFunction036 (RandomKey, 256)) return RPC_S_OUT_OF_MEMORY; // Initialize the RC4 instance rc4_safe_key (& g_rc4SafeCtx, InstNum, 256, (BYTE *) RandomKey); }; // Generate UUID value using RC4 algorithm rc4_safe (& g_rc4SafeCtx, InstNum, cbData, pbData); return RPC_S_OK; }; // Select RC4 instance void rc4_safe_select ( pRC4_CONTEXT pRC4Ctx, DWORD * pInstNum, DWORD * pAccumulator) { g_rc4TotalRequests ++; * pInstNum = g_rc4TotalRequests & (pRC4Ctx-> InstanceCount - 1); * pAccumulator = pRC4Ctx-> Rc4Ins [* pInstNum] -> Accumulator; }; // Thread-safe wrapper for RC4 call void rc4_safe (pRC4_CONTEXT pRC4Ctx, DWORD InstNum, DWORD cbData, BYTE * pbData) { EnterCriticalSection (& pRC4Ctx-> Rc4Ins [InstNum] -> CS); rc4 (& pRC4Ctx-> Rc4Ins [InstNum] -> State, cbData, pbData); LeaveCriticalSection (& pRC4Ctx-> Rc4Ins [InstNum] -> CS); }; // Classic implementation of the RC4 algorithm void rc4 (PRC4_INSTANCE_STATE pRC4State, DWORD cbData, BYTE * pbData) { BYTE t = 0; for ( int p = 0; p <cbData; p ++) { pRC4State-> i = (pRC4State-> i + 1) & 0x0FF; pRC4State-> j = (pRC4State-> j + pRC4State-> SBOX [pRC4State-> i]) & 0x0FF; t = pRC4State-> SBOX [pRC4State-> i]; pRC4State-> SBOX [pRC4State-> i] = pRC4State-> SBOX [pRC4State-> j]; pRC4State-> SBOX [pRC4State-> j] = t; * (pbData + p) = * (pbData + p) ^ pRC4State-> SBOX [(pRC4State-> SBOX [pRC4State-> i] + pRC4State-> SBOX [pRC4State-> j]) & 0xFF]; }; }; |
Obviously, there are no irreversible transformations in the algorithm of the UUID generator, and it is based on the stream cipher RC4 [6]. The global structure g_rc4SafeCtx, containing 8 independent instances of the stream cipher states (hereinafter referred to as instances), and the global counter g_rc4TotalRequests, on the basis of which the switching between them occurs, completely describes the state of the entire generator. Each instance has its own S-box and stores the values of two cipher registers. When you call GenerateRandomNumber, the next instance of RC4 is selected (with index (g_rc4TotalRequests + 1) mod 8), which should ensure uniform utilization of S-boxes.
NOTE Since rpcrt4.pdb is not very talkative, especially when it comes to global variables, I assigned a name to the g_rc4TotalRequests counter that more or less corresponds to its intended purpose. |
Formally, the generation algorithm can be represented as a diagram shown in Figure 1.
When you want to create the i-th in a row UUID-value ( U i ), the generator performs the following actions:
When rpcrt4.dll is loaded, the function rc4_safe_startup is called. It allocates from the heap of the target process the required memory capacity for the g_rc4SafeCtx structure, sets the value of all batteries to 0xFFFFFFFF, and initializes the critical sections of RC4 instances. Thus, each process will have its own state of the Uuid generator.
The instance S-block is initialized when it is first accessed or when the counter byte of the output (Accumulator) of this instance begins to exceed 500,000 bytes. Therefore, before the entire generator is reinitialized, at least 250,000 Uuid values (8 instances * 500,000 bytes = 16 bytes * 250,000 Uuid values) must be created with it. It is also important that the state of the generator can be considered fully defined only after the process calls UuidCreate 8 or more times.
RPC_STATUS GenerateRandomNumber (BYTE * pbData, DWORD cbData) { // Total number of bytes of output of the current RC4 instance // since its initialization DWORD Accumulator = 0; // Number of the current RC4 instance DWORD InstNum; // Buffer for RC4 key, if initialization is required PVOID RandomKey; rc4_safe_select (& g_rc4SafeCtx, & InstNum, & Accumulator); if (Accumulator> = 500000) { //Initialization // Generate a pseudo-random key for RC4 if (! SystemFunction036 (RandomKey, 256)) return RPC_S_OUT_OF_MEMORY; // Initialize the RC4 instance rc4_safe_key (& g_rc4SafeCtx, InstNum, 256, (BYTE *) RandomKey); }; // The full code of this function was given in the previous section of the article, so // here we will limit ourselves only to the section relating to initialization ... }; |
The mysterious function SystemFunction036 from advapi32.dll has another, more intelligible name - RtlGenRandom (“Georgy Ivanovich, he's Goga, he's Gosh, he's Yuri, he's Jora, does he live here?”). It is under this sign that it can be found in the MSDN annals [7]:
BOOLEAN RtlGenRandom ( __out PVOID RandomBuffer, __in ULONG RandomBufferLength ); |
The function is able to directly access Windows PRNG to get a pseudo-random byte sequence and, unlike CryptGenRandom, does not require the indication of a crypto-provider. The fact that SystemFunction036 is the shortest path to the generator is easy to follow with a debugger (the listing shows a call stack from CryptGenRandom):
> advapi32.dll!_SystemFunction036@8 () rsaenh.dll!_FIPS186Gen@28 () + 0x5f bytes rsaenh.dll!__CPGenRandom@12 () + 0x33 bytes advapi32.dll!_CryptGenRandom@12 () + 0x3d bytes |
Guided by popular wisdom "the farther into the forest, the fatter the guerrillas," we will not (at least for now) delve into the wilds of the PRNG Windows device. First, describing the rather complicated CryptGenRandom algorithm here does not make much sense, since the proposed attack model has nothing to do with the OS pseudo-random number generator. Secondly, researchers from The Hebrew University of Jerusalem in their work " Cryptanalysis of the Number Generator of the Windows Operating System"[8] coped with this task much better by examining the structure of the PRNG, as they say, on the shelves. The only thing worth paying attention to is the striking similarity of both generators - both UuidCreate and CryptGenRandom support 8 instances of RC4, which they use in a round-robin manner.
Another function, rc4_key, directly involved in the initialization of an S-block of an instance, is called in the critical section rc4_safe_key and is a classic implementation of the RC4 Key Scheduling Algorithm (KSA) [6]. The code responsible for the key tiling (if the key length is less than 256 bytes, the key, according to the KSA algorithm, is repeated until the entire 256-byte key array is filled) was omitted in order to save space (the length of the key transmitted in rc4_key from GenerateRandomNumber is always 256, so the rejected code does not play any role).
// Thread-safe wrapper for calling KSA void rc4_safe_key ( pRC4_CONTEXT pRC4Ctx, DWORD InstNum, DWORD cbData, BYTE * pbData) { EnterCriticalSection (& pRC4Ctx-> Rc4Ins [InstNum] -> CS); // RC4 Key Scheduling Algorithm (KSA) rc4_key (& pRC4Ctx-> Rc4Ins [InstNum] -> State, cbData, pbData); LeaveCriticalSection (& pRC4Ctx-> Rc4Ins [InstNum] -> CS); }; // RC4 Key Scheduling Algorithm (KSA) void rc4_key (PRC4_INSTANCE_STATE pRC4State, DWORD cbData, BYTE * pbData) { BYTE t = 0; for (pRC4State-> i = 0; pRC4State-> i <256; pRC4State-> i ++) pRC4State-> SBOX [pRC4State-> i] = pRC4State-> i; pRC4State-> j = 0; for (pRC4State-> i = 0; pRC4State-> i <256; pRC4State-> i ++) { pRC4State-> j = ( pRC4State-> j + pRC4State-> SBOX [pRC4State-> i] + (* (pbData + pRC4State-> i)) ) & 0x0FF; }; t = pRC4State-> SBOX [pRC4State-> i]; pRC4State-> SBOX [pRC4State-> i] = pRC4State-> SBOX [pRC4State-> j]; pRC4State-> SBOX [pRC4State-> j] = t; }; |
Thus, we arrive at the following scheme for initializing RC4 instances:
I would like to look into the eyes of a Hindu who used the safe word in the name of the function rc4_ safe _select and didn’t take care of the threads to synchronize when accessing the global variable g_rc4TotalRequests in her body. Either the sclerosis of the young man overcame, or the matter tended to be released - we are unlikely to find out now. But we can admire the effects.
Figure 2 shows the situation when two competing streams access rc4_safe_select and under certain circumstances get the same InstNum and Accumulator values.
The consequences are obvious:
The described error is not so critical as to fertilize the bald spot with ashes or dig in the nearest cave in anticipation of the apocalypse. However, getting rid of it will not be superfluous at all, especially since it can be done with a light stroke of the pen:
// Select RC4 instance void rc4_safe_select ( pRC4_CONTEXT pRC4Ctx, DWORD * pInstNum, DWORD * pAccumulator) { DWORD l_RC4TotalRequests; // Deadlock on incrementing l_RC4TotalRequests = InterlockedIncrement (& g_rc4TotalRequests); * pInstNum = l_RC4TotalRequests & (pRC4Ctx-> InstanceCount - 1); * pAccumulator = pRC4Ctx-> Rc4Ins [* pInstNum] -> Accumulator; }; |
or the same, but for art lovers:
// Select RC4 instance void rc4_safe_select ( pRC4_CONTEXT pRC4Ctx, DWORD * pInstNum, DWORD * pAccumulator) { DWORD l_RC4TotalRequests; __asm { mov eax , 1 lock xadd g_rc4TotalRequests, eax inc eax mov l_RC4TotalRequests, eax }; * pInstNum = l_RC4TotalRequests & (pRC4Ctx-> InstanceCount - 1); * pAccumulator = pRC4Ctx-> Rc4Ins [* pInstNum] -> Accumulator; }; |
You can learn more about deadlock functions in MSDN [9].
The determinism inherent in the software (and in some cases hardware) implementation of any algorithm is the true Achilles heel of cryptographic systems. And sadly enough, it is the implementation errors that discredit the protected environment more often than the vulnerabilities found in mathematical models. The matter is further complicated by the fact that developers sometimes deliberately refuse to use persistent protection mechanisms, explaining this, for example, by the inability to ensure maximum speed combined with an acceptable level of security. Moreover, the “contract with conscience” clause is not limited to the clause on impaired productivity. With no less success, other “obstructive arguments” are also going on. Let us recall at least our remote attack on MS SQL Server [10] with Jan Lieberman, which we would not be able to do, Do not be secure in the TDS protocol sacrificed to the “feature” of backward compatibility with less protected clients. So traces of compromises can be found in almost any applied solution and the results of the UuidCreate study confirm this once again.
A cryptographically reliable pseudo-random number generator should not only produce a sequence that seems seemingly random at first glance, but also have two important properties - Backward and Forward Security.
Backward security - does not allow an attacker to predict the states in which he will be in the future according to the current state of the generator. To reduce the detrimental effect of determinism and to at least somehow meet this requirement, the algorithm usually provides for regular state updates ( rekey) a true random bit sequence. And the more often it is executed, the more resistant is the generator. Entropy sources can be indications of performance counters, measurements of thermal sensors, processor registers values at a certain point in time, and other values remaining secret to an intruder with seven seals (refer to the book by Michael Howard and David Leblank Protected Code [11], where the authors without regretting the paper, more than a hundred sources for CryptGenRandom were listed separated by commas
Now we already know from what test the Uuid generator was made and with confidence we can say - it does not stand the test for backward security . A full status update occurs after 4 · 10 6 bytes of output (8 instances of RC4 * 500,000 bytes), so an attacker who managed to get the “cast” of all the registers and S-blocks of UuidCreate at a certain point in time can predict up to 250,000 future Uuid -values Moreover, the generator's GPS entropy is represented by SystemFunction036, which, given the predictability of the latter [8], increases the length of the compromised data stream to an extreme 256 · 10 6 bytes!
Indeed, CryptGenRandom, like UuidCreate, consists of 8 instances of RC4. Each of them goes through the re-initialization procedure, when the output byte counter begins to exceed 16 KB. This is almost 30 times less than that of the Uuid-generator, but not enough to be considered reliable. RC4 instances in CryptGenRandom are used evenly, therefore, a full update of the PRNG state is performed only after 128 KB of output (8 instances of RC4 * 16 KB).
The rc4_safe_key function consumes 256 bytes of the Windows PRNG exit per one reinitialization of an instance in the Uuid generator, while 2 KB is spent on a full update (8 instances * 256 bytes of the key). By simple arithmetic operations, it is easy to calculate how many Uuid-values an attacker can predict if he can get the current state of CryptGenRandom:
4 · 10 6 * (128 Kb / 2 Kb) = 256 · 10 6 bytes;
256 · 10 6 bytes / 16 bytes in one Uuid-value = 16 · 10 6 Uuid-values
You will ask why it was necessary to use a surrogate derived from PRNG Windows instead of sources of entropy of the OS. I'll start from afar. First of all, no one in their right minds placed great hopes on the resilience of UuidCreate (and those who have laid are simply obliged to read this article to the end). The demiurgists themselves clearly stated their position in RFC4122 [1]: “Do not assume that the UUIDs are hard to guess; they should not be used as security requirements, for example. A predictable random number source will exacerbate the situation ». So developers never had a goal to create a safe Uuid generator. Unfortunately, the planet Earth is still trampling on goblins (not only trampling, but also express payment cards are stamped!), Which apply to such warnings without proper trepidation.
Secondly, for unnecessary access to the system registry key, where the 80-byte Seed is stored, or calling the function of the KSecDD driver, you will have to pay with expensive CPU time ticks (.NET chikam cannot understand;) This will be especially noticeable in the latter case, since it requires switching to kernel mode (here's a compromise between performance and security). Third, it is much easier to create a pseudo-random key for RC4 using SystemFunction036 rather than complicate your life with the overly complicated generator design.
We now turn to the second property of reliable PRNG - Forward Security . Its meaning is that the attacker who owns the current state of the generator should not extract any information about the previous output from it. In other words, PRNG should be designed as a unidirectional function, using hashing algorithms for this purpose.
But here, UuidCreate fails. The only transformations that we see in the code are limited to permutations inside S-boxes. An attacker can easily perform these operations in the reverse order and obtain the preceding gamma of the generator:
void rc4_revert (PRC4_INSTANCE_STATE pRC4State, DWORD cbData, BYTE * pbData) { BYTE t = 0; for ( int p = cbData - 1; p> = 0; p--) { * (pbData + p) = * (pbData + p) ^ pRC4State-> SBOX [(pRC4State-> SBOX [pRC4State-> i] + pRC4State-> SBOX [pRC4State-> j]) & 0xFF]; t = pRC4State-> SBOX [pRC4State-> i]; pRC4State-> SBOX [pRC4State-> i] = pRC4State-> SBOX [pRC4State-> j]; pRC4State-> SBOX [pRC4State-> j] = t; pRC4State-> j = (pRC4State-> j - pRC4State-> SBOX [pRC4State-> i]) & 0x0FF; pRC4State-> i = (pRC4State-> i - 1) & 0x0FF; }; }; |
So, suppose the attacker took a memory “cast” from the g_rc4SafeCtx structure and remembered the g_rc4TotalRequests call count at a time t such that T 1 ? t? T 2 , where T 1 is the time of the last complete generator update, and T 2 is the time of the upcoming update. Then it can reproduce the output of the generator in the interval [T 1 ; t) and predict the values returned by the UuidCreate function until the occurrence of T 2 .
There is, however, one nuance that must be taken into account. The value of the buffer I i transmitted to UuidCreate in the i-th call is not always known to the attacker. And according to the scheme shown in Figure 1, an XOR operation is performed between this value and the gamut of RC4. Therefore, at first glance, it seems that an attacker who does not know I i will hardly be able to use the proposed attack model. However, in practice, the Uuid variable is either initialized with zeros, or it contains garbage that is on the stack by the time the memory is allocated. In the latter case, the probability of occurrence of correlations between different values of I i in the interval from T 1 to T 2 It remains quite high, than the attacker will not fail to take advantage of (I will show it a little lower using the example of MS SQL Server).
Now that you are familiar with the anatomy of UuidCreate and have mastered the theoretical part of the article, I would like to say a few words about the console utility Uuid Big Brother , which I wrote to automate the attack procedure. The utility is able to take a dump of the state of the Uuid-generator in any of the OS processes and to guess both the previous and the subsequent Uuid-values (for a detailed description of the UBB parameters, see Appendix B).
As a warm-up, I propose to hone our oracle skills on a simple guidgen.exe application, from time immemorial included in the delivery of Visual Studio [12]. This utility is noteworthy because before calling UuidCreate, the buffer for the Uuid-value is initialized with zeros (that is, I i == NULL). You can see this for yourself since the guidgen source code is available for download to everyone [13]:
// Guidgdlg.h void CGuidGenDlg :: OnNewguid () { // create random GUID using uUidCreate so that we // can check for more error codes m_guid = GUID_NULL; HRESULT hr = :: UuidCreate (& m_guid); // ... } |
This is definitely a chance to exercise in foresight.
ubb -d3912 "c: \ ubb \ guidgen.dmp"
|
The result of the command will be approximately as follows:
Warning: not all the RC4 instances were initialized. Please initiate 8 UUIDs generation. State was successfully dumped to file c: \ ubb \ guidgen.dmp. |
UBB hints to us that the process did not have time to initialize one or more RC4 instances, so part of the predicted Uuid values will not be true.
ubb -d3912 "c: \ ubb \ guidgen.dmp"
--------------------------------
State was successfully dumped to file c: \ ubb \ guidgen.dmp.
|
ubb -g "c: \ ubb \ guidgen.dmp" -------------------------------- EAC00EFD-68DB-449F-9350-8954C2300DA2 D2DD36ED-6620-4CC9-80BE-9569EFD00FA5 D60CF120-A6DA-4B4B-84F1-2C1BD6EDC071 1F00F7F3-8F0F-4473-80F2-D3B175CC93DB ADA0FB01-85C0-4D67-86D1-925C68373F22 82D781D6-489A-46D6-9F9F-47265F76DB38 F10E5C71-5561-4999-9AF7-16CB1D80B0A0 050E5489-AAB3-4837-A240-92B4B165D588 |
Pressing any key in UBB results in new values. By default, the utility issues them in portions of 8 elements each, but you can change this number using the –o key (for more details, see Appendix B).
The time has come to recollect Vasily Razdolbayev and find out how he managed to circle the children from the White Collars department around his finger. But first, make sure that the T-SQL function NEWID () actually uses the Windows UUID generator, and that the attack technique described here is applicable to scenarios with express payment cards. The listing shows the recovered code for MS SQL Server 2008 CTP5 (build 1075):
; sqlservr.exe ; void __fastcall GuidNewid (class CEsExec *, class CXVariant *) ? GuidNewid @@ YIXPAVCEsExec @@ PAVCXVariant @@@ Z proc near mov edi , edi push esi mov esi , edx lea eax , [ esi +4] push eax ; pguid call __imp__CoCreateGuid @ 4 ; CoCreateGuid (x) test eax , eax jnz short loc_1A07597 mov [ esi ], al loc_1A07597: pop esi retn ? GuidNewid @@ YIXPAVCEsExec @@ PAVCXVariant @@@ Z endp ; ole32.dll ; long __stdcall wCoCreateGuid (struct _GUID *) ? wCoCreateGuid @@ YGJPAU_GUID @@@ Z: mov edi , edi push ebp mov ebp , esp push esi push [ ebp + lp] ; Uuid call ds : __ imp__UuidCreate @ 4 ; UuidCreate (x) mov esi , eax cmp esi , 720h jz short loc_776A5659 test esi , esi jnz loc_776EDF0E loc_776A5659: xor eax , eax loc_776A565B: pop esi pop ebp retn 4 _CoCreateGuid @ 4 endp |
The next step is to figure out how the Ii buffer is initialized before running UuidCreate. To do this, we need SQL Internals Profiler - a tool developed by Jan Lieberman and indispensable in cases where you need to collect statistics on calls to the internal functions of MS SQL Server. Now, in particular, we will be interested in the values passed by the pguid pointer to the CoCreateGuid.
- Enable interception of the CoCreateGuid function by splicing
exec xp_SQLInternalsProfiler_AddSource_Splicing 'ole32.dll',
'CoCreateGuid', 'hpg1; -', 1
|
Translated from Liberman, the phrase 'hpg1; -' sounds like this: “The first parameter (1) of the function is a reference (p) to an archival value 16 bytes long (h) that needs to be intercepted before calling CoCreateGuid (;-). Group results by unique values (g) ". Those who wish to master this language can recommend an excellent Russian-Liberian phrasebook [14,15].
Let's see how NEWID behaves if you periodically contact it:
- We reset the accumulated exec xp_SQLInternalsProfiler_ClearAll statistics print NEWID () waitfor delay '00: 00: 05 ' print NEWID () waitfor delay '00: 00: 05 ' print NEWID () waitfor delay '00: 00: 05 ' print NEWID () waitfor delay '00: 00: 05 ' print NEWID () - Display the result EXECUTE xp_SQLInternalsProfiler_Results 147 |
I got the following picture (SQL Server 2008 CTP5):
source_name | event_id | binary_grpup_id | count |
---|---|---|---|
ole32.dll CoCreateGuid |
Before_call | 0xF9E30A01 3061 AE0400000000 C061 AE04 | one |
ole32.dll CoCreateGuid |
Before_call | 0xF9E30A01 3863 AE0400000000 C863 AE04 | one |
ole32.dll CoCreateGuid |
Before_call | 0xF9E30A01 4065 AE0400000000 D065 AE04 | one |
ole32.dll CoCreateGuid |
Before_call | 0xF9E30A01 4867 AE0400000000 D867 AE04 | one |
ole32.dll CoCreateGuid |
Before_call | 0xF9E30A01 5069 AE0400000000 E069 AE04 | one |
We see that I i (the binary_grpup_id field) was changed every time the NEWID function was called, although the difference in values is not so great - only 25% (4 bytes from 16 are bold). The server does not bother to initialize * pguid, so the garbage is constantly being transmitted to CoCreateGuid. Thus, for example, the double word 0xF9E30A01 is the return address from CreateExecValSeg:
010AE3F4 call CEsCompValSeg :: CreateExecValSeg 010AE3F9 pop edi ... |
Or here's another funny arithmetic (subtract the second double word from the fourth):
0x04AE61C0 - 0x04AE6130 = 0x90 0x04AE63C8 - 0x04AE6338 = 0x90 0x04AE65D0 - 0x04AE6540 = 0x90 ... |
The correlation found reduces the entropy level I i by half (from 2 32 to 2 16 ), since the number of independent unknowns has decreased. You shouldn’t disregard the “vertical” pattern (we subtract the second double word on i + 1 iteration from it on i-iteration):
0x04AE6338 - 0x04AE6130 = 0x208 0x04AE6540 - 0x04AE6338 = 0x208 0x04AE6748 - 0x04AE6540 = 0x208 ... |
So, if we recognize I i , then we can easily calculate I i + 1 . Moreover, any I i unpredictable are only 2 bytes.
Restart MS SQL Server and repeat the experiment:
source_name | event_id | binary_grpup_id | count |
---|---|---|---|
ole32.dll CoCreateGuid |
before_call | 0xF9E30A01 30E18406 00000000 C0E18406 | one |
ole32.dll CoCreateGuid |
Before_call | 0xF9E30A01 38E38406 00000000 C8E38406 | one |
ole32.dll CoCreateGuid |
Before_call | 0xF9E30A01 40E58406 00000000 D0E58406 | one |
ole32.dll CoCreateGuid |
Before_call | 0xF9E30A01 48E78406 00000000 D8E78406 | one |
ole32.dll CoCreateGuid |
Before_call | 0xF9E30A01 50E98406 00000000 E0E98406 | one |
The new results indicate that 8 bytes remain unchanged, and the correlations persist after the restart.
Of course, fully relying on these observations would be too rash. Let's try to simulate a situation where Uuid identifiers are not only created, but also inserted into the table field:
create table #pin_codes ( id int identity , pin_code varchar (36) ) go - We reset the accumulated exec xp_SQLInternalsProfiler_ClearAll statistics declare @i int = 0 while (@i! = 10000) begin insert #pin_codes values (newid ()) set @i + = 1 end - Display the result EXECUTE xp_SQLInternalsProfiler_Results 147 |
It turns out that any use of NEWID in cyclic operations (not necessarily when inserted into a table) demonstrates an enviable constancy of the values of Ii. Over 10,000 iterations, it changed only once!
source_name | event_id | binary_grpup_id | count |
---|---|---|---|
ole32.dll CoCreateGuid |
Before_call | 0x0000000000000000B4F5C36200000000 | 9999 |
ole32.dll CoCreateGuid |
Before_call | 0x00000000FFFFFFFF00000000FA700201 | one |
It is very unlikely that PIN codes for express payment cards were generated “individually”, as in the first experiment. Therefore, the asocial element Razdolbaev will appreciate this gift of fate. However, the total entropy of the set {I 1 , I 2 , ... I n } in both cases will be the same - 2 32 . It would be possible to mention other ways to call NEWID, but almost all of them (there are exceptions, I will not lie) come down either to the statistics collected in the experiment with wait for, or to the results of the exercise with the cycle. Similar tests for MS SQL Server 2000/2005 reveal approximately the same correlations.
When single calls to NEWID we observe "vertical" pattern that allows us I of i calculate {I of i + 1 , I of i + 2 , ... I of i + n }. And if the function was used in a cycle, then I i = I i + 1 = I i + 1 = ... = I i + n . Therefore, to restore the entire sequence, an attacker needs to calculate at least one value I i . And here he comes to the aid of the property of reversibility of the operation XOR. In the generator algorithm, there is a stage where an exclusive OR is performed between the gamma RC4 O i and the value of I i :
C i = O i XOR I i => I i = O i XOR C i
U i [1..6] = C i [1..6]
U i [7] = (C i [7] & 0x0F) | 0x40
U i [8] = C i [8]
U i [9] = (C i [9] & 0x3F) | 0x80
U i [10..16] = C i [10..16]
We introduce I ' i = O i XOR U i , where I' i differs from I i by several bits in 7 and 9 bytes.
We show that replacing I i with I ' i in direct calculations gives the same value U i :
C ' i = O i XOR I' i
C ' i [1..6] = C i [1..6] = U i [1..6]; C ' i [8] = C i [8] = U i [8]; C ' i [10..16] = C i [10..16] = U i [10..16];
C ' i [7]? C i [7]; C ' i [9]? C i [9], but
U ' i [7] = (C' i [7] & 0x0F) | 0x40 = (C i [7] & 0x0F) | 0x40 = U i [7]
U ' i [9] = (C' i [9] & 0x3F) | 0x80 = (C i [9] & 0x3F) | 0x80 = U i [9]
Hence, U ' i = U i and the replacement of I' i by I i is valid.
Suppose Razdolbaev bought one payment card in the nearest stall and found the PIN code U k under a protective layer . Our calculations will help him calculate I k only if he knows at what iteration of the Uuid generator the given PIN code was received. In other words, Razdolbaev should know k. This problem can be solved by resorting to the enumeration of the values of O i generated by the generator in the interval between two complete state updates [T 1 ; T 2 ]. And now Vasily will have to fork out not for one, but for as many as three payment cards (PIN codes P x , P y and P z ).
BOOL InOrder = FALSE; For i from 1 to 250 000 { For j from 1 to 250 000 { If (Oi XOR Px) == (Oj XOR Py) { // Perhaps x = i, y = j For k from 1 to 250 000 { If (Oi XOR Px) == (Ok XOR Pz) { // x = i, y = j, z = k // Ii = Ij = Ik = (Oi XOR Px) InOrder = TRUE; } } If (! InOrder) { // x = j, y = i, z = k // Ii = Ij = Ik = (Oi XOR Py) } } } } |
The method is based on the assumption that with each subsequent call to UuidCreate, the value of i i does not change. However, the same idea is easy to adapt to the case when I i remains constant only a few bytes. For example, for single calls to NEWID enough to treat the '=' sign of equality any 8-byte values of 16 are limited only part I of i , we are in the long run no risk, because the probability of the existence About i and About j such that (About i XOR P x ) = [8] (O j XOR P x ) with i? j is negligible.
Values that satisfy the condition (O i XOR P x ) = (O j XOR P y ) will also satisfy (O i XOR P y ) = (O j XOR P x ), and (O i XOR P x )? (O i XOR P y ). To determine which of the two options is correct, it is necessary to perform another morning run over the set {O i }:
We will set up an investigative experiment, where we will play the role of Vasya Razdolbaev.
ubb –d1508 "c: \ ubb \ sqlservr.dmp" |
Razdolbaev should have done these two steps on April 14, on the eve of the PIN code generation procedure.
create table #pin_codes ( id int identity, pin_code varchar (36) ) go set nocount on declare @i int set @i = 0 while (@i! = 10000) begin insert #pin_codes values (newid ()) set @i = @i + 1 end |
Each value should be located on a new line.
ubb -g -ik "known_uuid.txt" -ab8 -of "pins.txt" "sqlservr.dmp" ---------------------------------- Analysis started. Press <Ctrl> + <Break> to abort. Analyzing ... | Known UUID # 1 index: 1594 Known UUID # 1: 9EF15183-EFB9-4FCC-B9A5-5321E7A15385 Gen output # 1: 9EF15183-EFB9-4FCC-8D50-0742E7A15385 Initial val # 1: 00000000-0000-0000-34F5-546300000000 Known UUID # 2 index: 73 Known UUID # 2: 7D7EE0E8-3BE3-409D-B56B-BF6118F48C33 Gen output # 2: 7D7EE0E8-3BE3-409D-819E-EB0218F48C33 Initial val # 2: 00000000-0000-0000-34F5-546300000000 Known UUID # 3 index: 6101 Known UUID # 3: 7F7DF678-FD5A-418F-BBC2-289031D79174 Gen output # 3: 7F7DF678-FD5A-418F-8F37-7CF331D79174 Initial val # 3: 00000000-0000-0000-34F5-546300000000 Common initial: 00000000-0000-0000-34F5-546300000000 Uuids was successfully saved to file pins.txt. |
Known UUID #n: string n in the file known_uuid.txt.
Known UUID #n index: the sequence number i of the UuidCreate call from the moment the "cast" of the generator state was taken.
Gen output #n: Oi corresponding to the given Pi.
Initial val #n: Ii corresponding to the given Pi.
Check how accurate the predictions are. Select the first value you like from the table and look for it in the pins.txt file.
The console utility Uuid Big Brother should be used exclusively for educational, health and recreational purposes. The author is not responsible for any damage, directly or indirectly caused by using it.
Download the utility along with the installer
Download utility without installer (console + VC runtime library)
Parameter descriptions and application tips can be found in Appendix B.
SQL Internals Profiler, version 1.0.3, © Yan Liberman
In order for the utility to be able to gain unhindered access to the memory of the external process, it must be run on behalf of an account with the authority of Debug Programs (for example, on behalf of the local system administrator).
ubb [<switches> ...] <dump_file> |
Key | Description | Example |
---|---|---|
-h | Displays a description of utility parameters to the console. | ubb –h |
-e | Displays usage examples of the utility in the console. | ubb –e |
Combining information keys with other parameters is not allowed.
Key | Description | Example |
---|---|---|
-d {Process id} <dump_file> | Removes the state of the Uuid generator from the specified process and saves it to the dump_file file. The {Process Id} value of the target process corresponds to the value in the PID column of the Task Manager. | ubb –d1234 uuid_state.dmp |
-b | Displays debug information. Can only be used with the –d key. | ubb –d1234 –b uuid_state.dmp |
If not all RC4 instances are initialized by the process (less than 8 calls to UuidCreate were made during its operation), you will receive the message:
Warning: not all the RC4 instances were initialized. Please initiate 8 UUIDs generation. |
This warning means that the state of only those instances that are already initialized at the time of recording and therefore their output is predefined is saved to the dump file. The sequence of UUID values predicted by the utility based on an incomplete dump file will contain “gaps”. Often, this situation occurs at the very beginning of the target process or in situations where the process very rarely calls UuidCreate.
If the operation is successful, the following message is displayed:
State was successfully dumped to file <dump_file>. |
Key | Description | Example |
---|---|---|
-g {f <cnt> | b <cnt>} <dump_file> | Loads the state of the generator from dump_file and calculates the first cnt of the subsequent (f) or previous (b) UUID values relative to it. If the cnt parameter is not specified or is 0, UBB will calculate the entire possible sequence of values. Calculations will continue until the output byte counter for each RC4 instance (Accumulator) becomes 0 (b flag) or 500,000 (for f flag). If the –g key is specified without additional parameters, the default values will be applied , namely: -gf0. | ubb –gf35 uuid_state.dmp |
-o {c <display_count> | f <uuids_file>} | Determines where to output the calculated sequence of UUID-values.c <display_count> - displays the UUIDs found in the console in portions of display_count values, waiting for each key to press any key after each portion. By default, the display_count parameter is 8.f <uuids_file> - saves the calculated sequence to the uuid_file text file. If the –o options are not specified, the default values will be: -oc8. | Output to console: ubb –gf100 –oc25 uuid_state.dmp Output to the file: ubb –gf100 –of ”uuids.txt” uuid_state.dmp |
-i {0x <value> | k <known_uuid_file>} | Allows you to set or calculate the value of I i , which initializes the parameter * Uuid before calling the function UuidCreate (* Uuid) .0x <value> - assigns the value to all 16 bytes I i (I i [1] = I i [2] = .. . = I i [16] = value). value should not exceed 0xFF.k <known_uuid_file> - calculates I i by the three known Uuid-values specified in the file known_uuid_file. Each value must be placed on a new line (separator 0x0D0A). If the parameter is not specified, the option –i0x0 is valid by default. | All bytes I i are 0xFF: ubb -g –i0xFF "sqlservr.dmp" Ii must be calculated from the three known P x , P y , P z : ubb-g -ik "known_uuid.txt" "sqlservr.dmp" |
-au {<uuid_count>} | Limits the number of Uuid values that are used to calculate I i . It makes sense to specify this key only in combination with -ik <known_uuid_file>. By default, uuid_count = 250,000. | ubb -g -ik "known_uuid.txt" –au7500 "sqlservr.dmp" |
-ab {<collisions>} | Determines the accuracy of the comparison operator when selecting I i . I x = I y if they have any collisions bytes. The number of collisions should not exceed 16. By default, it is 8. | Read I x = I y if any 10 bytes I x match any 10 bytes I y : ubb -g -ik "known_uuid.txt" –ab10 "sqlservr.dmp" |
operating system | Version of the library rpcrt4.dll |
---|---|
Windows Server 20035.2.3790 Service Pack 2 Build 3790 | 5.2.3790.3959 |
Windows XP5.1.2600 Service Pack 2 Build 2600 | 5.1.2600.2180 |
Windows Vista6.0.6000 Build 6000 | 6.0.6000.16386 |
Windows Server 20086.0.6001 Beta 3, v.126 Build 6001 | 6.0.6001.16510 |
Windows Server 20086.0.6001 Build 6001 | 6.0.6001.18000 |
"Cryptanalysis for Windows Operating System", Leo Dorrendorf, Zvi Gutterman, Benny Pinkas
Interlocked Variable Access (MSDN)
"Protected Code", Michael Howard, David LeBlanc
Visual Studio guidgen.exe (MSDN)
GUIDGEN Sample: Generates Globally Unique Identifiers (MSDN)
SQL Internals Profiler, Jan Lieberman
SQL Internals Profiler (usage examples), Jan Lieberman
Copyright © 1994-2016 K-Press Ltd.